package erase

import (
	"context"
	"sync"

	bytedtracer "code.byted.org/bytedtrace/interface-go"
	v1 "code.byted.org/bytertc/whiteboard_server/biz/api/pb/v1"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/biz_error"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/trace"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action/base"
	"code.byted.org/bytertc/whiteboard_server/biz/service/object"
	"code.byted.org/gopkg/gopool"
	"code.byted.org/gopkg/logs/v2/log"
	"github.com/pkg/errors"
	"google.golang.org/protobuf/proto"
)

func NewAction(ctx context.Context, baseAct base.Action, raw *v1.Action) (*eraseAction, error) {
	eraseAct := &eraseAction{
		Action:    baseAct,
		rawAction: raw,
	}
	if raw.Type == v1.ActionType_kErase {
		erase := &v1.Erase{}
		err := proto.Unmarshal(raw.Data, erase)
		if err != nil {
			return nil, biz_error.DecodeError.Wrap(errors.WithStack(err))
		}
		if len(erase.ObjIds) == 0 {
			return nil, biz_error.InvalidParam.Wrap(errors.New("empty objIds"))
		}
		eraseAct.objectIds = erase.ObjIds
	}
	eraseAct.rawAction.Type = v1.ActionType_kErase
	return eraseAct, nil
}

type eraseAction struct {
	base.Action
	rawAction   *v1.Action
	objectIds   []string
	successObjs sync.Map
	currenIndex int
}

func (e *eraseAction) GetObjId() string {
	return e.objectIds[e.currenIndex]
}

func (e *eraseAction) GetActionId() int32 {
	return e.rawAction.ActionId
}

func (e *eraseAction) Execute(ctx context.Context) error {
	span, ctx := bytedtracer.StartCustomSpan(ctx, trace.INTERNAL, trace.EditingErase)
	defer span.Finish()

	if len(e.objectIds) == 0 {
		ids, err := e.Repo.ListElementIds(ctx, e)
		if err != nil {
			return err
		}
		e.objectIds = ids
		//clear 没法发上锁动作
		e.lockObjs(ctx)

	}

	objectsPerGoroutine := len(e.objectIds) / 10
	if objectsPerGoroutine < 5 {
		objectsPerGoroutine = 5
	}

	wg := sync.WaitGroup{}
	var start, end int
	for start = 0; start < len(e.objectIds); start = end {
		end = start + objectsPerGoroutine
		if end > len(e.objectIds) {
			end = len(e.objectIds)
		}
		st, en := start, end
		wg.Add(1)
		gopool.CtxGo(ctx, func() {
			defer wg.Done()
			iter := newIter(e, st, en)
			iter.execute(ctx)
		})
	}
	wg.Wait()
	e.unlockObjs(ctx)
	return e.genError()
}

func (e *eraseAction) RawActions() []*v1.Action {
	raw := &v1.Erase{}
	e.successObjs.Range(func(key, value interface{}) bool {
		raw.ObjIds = append(raw.ObjIds, key.(string))
		return true
	})

	e.rawAction.Data, _ = proto.Marshal(raw)
	return []*v1.Action{e.rawAction}
}

func (e *eraseAction) IsAtomic() bool {
	return false
}

func (e *eraseAction) ConflictObjects() []string {
	var objects []string
	for _, obj := range e.objectIds {
		if _, exist := e.successObjs.Load(obj); !exist {
			objects = append(objects, obj)
		}
	}
	return objects
}

func (e *eraseAction) genError() error {
	successCount := 0
	e.successObjs.Range(func(_, _ interface{}) bool {
		successCount++
		return true
	})
	if successCount != len(e.objectIds) {
		return biz_error.Conflict.Wrap(errors.New(""))
	}
	return nil
}

// 只有clear用
func (e *eraseAction) lockObjs(ctx context.Context) error {
	for idx := range e.objectIds {
		e.currenIndex = idx
		e.Repo.LockObject(ctx, e)
	}
	return nil
}

// 用
func (e *eraseAction) unlockObjs(ctx context.Context) error {
	for idx := range e.objectIds {
		e.currenIndex = idx
		e.Repo.UnlockObject(ctx, e)
	}
	return nil
}

func newIter(e *eraseAction, start, end int) *eraseActionIterator {
	return &eraseActionIterator{
		eraseAction:  e,
		currentIndex: start - 1,
		endIndex:     end,
	}
}

type eraseActionIterator struct {
	*eraseAction
	currentIndex int
	endIndex     int
}

func (e *eraseActionIterator) next() bool {
	e.currentIndex++
	return e.currentIndex < e.endIndex
}

func (e *eraseActionIterator) GetObjId() string {
	return e.eraseAction.objectIds[e.currentIndex]
}

func (e *eraseActionIterator) execute(ctx context.Context) {
	span, ctx := bytedtracer.StartCustomSpan(ctx, trace.INTERNAL, trace.EditingSubErase)
	defer span.Finish()
	span.SetTag("object_id", e.objectIds[e.currentIndex+1:e.endIndex])

	for e.next() {
		err := e.eraseObject(ctx)
		if err != nil {
			if !errors.Is(err, biz_error.Conflict) {
				log.V1.CtxError(ctx, "fail to erase object %s, %+v", e.GetObjId(), err)
			}
			continue
		}
		e.successObjs.Store(e.GetObjId(), struct{}{})
	}
}

func (e *eraseActionIterator) eraseObject(ctx context.Context) error {
	err := e.Repo.Authenticate(ctx, e)
	if err != nil {
		return err
	}
	ele, err := e.Action.Repo.GetElement(ctx, e)
	if err != nil {
		return err
	}
	err = e.Action.Repo.DeleteElement(ctx, e)
	if err != nil {
		return err
	}
	_ = object.DefaultElement{} //golint-ci 不识别下边*object报错，此处为了通过 golint-ci 检查
	switch v := ele.(type) {
	case *object.DefaultElement:
		switch v.ProtoObject.(type) {
		case *v1.Audio:
			_ = e.Repo.UnLockPageAudio(ctx, ele)
		case *v1.Video:
			_ = e.Repo.UnLockPageVideo(ctx, ele)
		}
	}
	return nil
}
